// (C) 2022 Intel Corporation. All rights reserved.
// Your use of Intel Corporation's design tools, logic functions and other 
// software and tools, and its AMPP partner logic functions, and any output 
// files from any of the foregoing (including device programming or simulation 
// files), and any associated documentation or information are expressly subject 
// to the terms and conditions of the Intel Program License Subscription 
// Agreement, Intel FPGA IP License Agreement, or other applicable 
// license agreement, including, without limitation, that your use is for the 
// sole purpose of programming logic devices manufactured by Intel and sold by 
// Intel or its authorized distributors.  Please refer to the applicable 
// agreement for further details.

`timescale 1ns/1ns
`default_nettype none

module four_way_fifo (
    input logic clk,
    input logic reset_n,

    input logic         port_a_write_req,
    input logic [31:0]  port_a_write_data,
    input logic         port_a_read_req,
    output logic [31:0] port_a_read_data,
    output logic        port_a_empty,
    output logic        port_a_full,

    input logic         port_b_write_req,
    input logic [31:0]  port_b_write_data,
    input logic         port_b_read_req,
    output logic [31:0] port_b_read_data,
    output logic        port_b_empty,
    output logic        port_b_full,

    input logic         port_c_write_req,
    input logic [31:0]  port_c_write_data,
    input logic         port_c_read_req,
    output logic [31:0] port_c_read_data,
    output logic        port_c_empty,
    output logic        port_c_full,

    input logic         port_d_write_req,
    input logic [31:0]  port_d_write_data,
    input logic         port_d_read_req,
    output logic [31:0] port_d_read_data,
    output logic        port_d_empty,
    output logic        port_d_full

);

// init value is the value before 0 because of roll over
// the read pointer points to the value that has been read last so on init it should be 0 minus
localparam RD_PTR_INIT_VAL = 7'd0;

// the write pointer points to the next addresss to be written to so its initialized to 0;
localparam WR_PTR_INIT_VAL = 7'd0;
logic [31:0]  ram_write_data;
logic [31:0] ram_read_data;
logic [7:0] ram_write_address;
logic [7:0] ram_read_address;
logic ram_write_enable;

logic [1:0] port_select;

logic [6:0] port_a_wr_ptr;
logic [6:0] port_a_rd_ptr;
logic [6:0] port_b_wr_ptr;
logic [6:0] port_b_rd_ptr;
logic [6:0] port_c_wr_ptr;
logic [6:0] port_c_rd_ptr;
logic [6:0] port_d_wr_ptr;
logic [6:0] port_d_rd_ptr;

logic [5:0] port_a_rd_ptr_reg;
logic [5:0] port_b_rd_ptr_reg;
logic [5:0] port_c_rd_ptr_reg;
logic [5:0] port_d_rd_ptr_reg;

logic [31:0] port_a_write_data_reg;
logic port_a_write_req_reg;
logic [31:0] port_b_write_data_reg;
logic port_b_write_req_reg;
logic [31:0] port_c_write_data_reg;
logic port_c_write_req_reg;
logic [31:0] port_d_write_data_reg;
logic port_d_write_req_reg;

logic port_a_write_req_dly;
logic port_b_write_req_dly;
logic port_c_write_req_dly;
logic port_d_write_req_dly;


// using a delay reg for an edge detect so that holding the signal too long 
// does not cause multiple writes
always_ff @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        port_a_write_req_dly <= 1'b0;
        port_b_write_req_dly <= 1'b0;
        port_c_write_req_dly <= 1'b0;
        port_d_write_req_dly <= 1'b0;
    end 
    else begin
        port_a_write_req_dly <= port_a_write_req;
        port_b_write_req_dly <= port_b_write_req;
        port_c_write_req_dly <= port_c_write_req;
        port_d_write_req_dly <= port_d_write_req;
    end
end



always_ff @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        port_select <= 2'b00;
    end 
    else begin
        port_select <= port_select + 1'b1;
    end
end

always_ff @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        port_a_wr_ptr <= WR_PTR_INIT_VAL;
        port_a_rd_ptr <= RD_PTR_INIT_VAL;
        port_b_wr_ptr <= WR_PTR_INIT_VAL;
        port_b_rd_ptr <= RD_PTR_INIT_VAL;
        port_c_wr_ptr <= WR_PTR_INIT_VAL;
        port_c_rd_ptr <= RD_PTR_INIT_VAL;
        port_d_wr_ptr <= WR_PTR_INIT_VAL;
        port_d_rd_ptr <= RD_PTR_INIT_VAL;
        port_a_write_req_reg <= 1'b0;
        port_b_write_req_reg <= 1'b0;
        port_c_write_req_reg <= 1'b0;
        port_d_write_req_reg <= 1'b0;
        port_a_rd_ptr_reg <= 6'h0;
        port_b_rd_ptr_reg <= 6'h0;
        port_c_rd_ptr_reg <= 6'h0;
        port_d_rd_ptr_reg <= 6'h0;

    end 
    else begin
        if (port_select == 2'b00 && port_a_write_req_reg && !port_a_full) begin
            port_a_wr_ptr <= port_a_wr_ptr + 1'b1;
            port_a_write_req_reg <= 1'b0;
        end
        if (port_a_write_req &!port_a_write_req_dly) begin
            port_a_write_data_reg <= port_a_write_data;
            port_a_write_req_reg <= 1'b1;
        end

        else if (port_select == 2'b01 && port_b_write_req_reg && !port_b_full) begin
            port_b_wr_ptr <= port_b_wr_ptr + 1'b1;
            port_b_write_req_reg <= 1'b0;
        end
        if (port_b_write_req&!port_b_write_req_dly) begin
            port_b_write_data_reg <= port_b_write_data;
            port_b_write_req_reg <= 1'b1;
        end
        else if (port_select == 2'b10 && port_c_write_req_reg && !port_c_full) begin
            port_c_wr_ptr <= port_c_wr_ptr + 1'b1;
            port_c_write_req_reg <= 1'b0;
        end
        if (port_c_write_req&!port_c_write_req_dly) begin
            port_c_write_data_reg <= port_c_write_data;
            port_c_write_req_reg <= 1'b1;
        end
        else if (port_select == 2'b11 && port_d_write_req_reg && !port_d_full) begin
            port_d_wr_ptr <= port_d_wr_ptr + 1'b1;
            port_d_write_req_reg <= 1'b0;
        end
        if (port_d_write_req&!port_d_write_req_dly) begin
            port_d_write_data_reg <= port_d_write_data;
            port_d_write_req_reg <= 1'b1;
        end

        if (port_a_read_req && !port_a_empty) begin
            port_a_rd_ptr <= port_a_rd_ptr + 1'b1;
            port_a_rd_ptr_reg <= port_a_rd_ptr[5:0];
        end
        else if (port_b_read_req && !port_b_empty) begin
            port_b_rd_ptr <= port_b_rd_ptr + 1'b1;
            port_b_rd_ptr_reg <= port_b_rd_ptr[5:0];
        end
        else if (port_c_read_req && !port_c_empty) begin
            port_c_rd_ptr <= port_c_rd_ptr + 1'b1;
            port_c_rd_ptr_reg <= port_c_rd_ptr[5:0];
        end
        else if (port_d_read_req && !port_d_empty) begin
            port_d_rd_ptr <= port_d_rd_ptr + 1'b1;
            port_d_rd_ptr_reg <= port_d_rd_ptr[5:0];
        end
    end
end

assign port_a_empty = (port_a_rd_ptr == port_a_wr_ptr);
assign port_a_full  = (port_a_rd_ptr[5:0] == port_a_wr_ptr[5:0]) && (port_a_rd_ptr[6] != port_a_wr_ptr[6]);
assign port_b_empty = (port_b_rd_ptr == port_b_wr_ptr);
assign port_b_full  = (port_b_rd_ptr[5:0] == port_b_wr_ptr[5:0]) && (port_b_rd_ptr[6] != port_b_wr_ptr[6]);
assign port_c_empty = (port_c_rd_ptr == port_c_wr_ptr);
assign port_c_full  = (port_c_rd_ptr[5:0] == port_c_wr_ptr[5:0]) && (port_c_rd_ptr[6] != port_c_wr_ptr[6]);
assign port_d_empty = (port_d_rd_ptr == port_d_wr_ptr);
assign port_d_full  = (port_d_rd_ptr[5:0] == port_d_wr_ptr[5:0]) && (port_d_rd_ptr[6] != port_d_wr_ptr[6]);

always_comb begin
    case (port_select) 
        2'b00: begin
            ram_write_address = {port_select, port_a_wr_ptr[5:0]};
            ram_read_address  = {port_select, port_a_rd_ptr_reg[5:0]};
            ram_write_enable  = port_a_write_req_reg & !port_a_full;
            ram_write_data    = port_a_write_data_reg;
        end
        2'b01: begin
            ram_write_address = {port_select, port_b_wr_ptr[5:0]};
            ram_read_address  = {port_select, port_b_rd_ptr_reg[5:0]};
            ram_write_enable  = port_b_write_req_reg & !port_b_full;
            ram_write_data    = port_b_write_data_reg;
        end
        2'b10: begin
            ram_write_address = {port_select, port_c_wr_ptr[5:0]};
            ram_read_address  = {port_select, port_c_rd_ptr_reg[5:0]};
            ram_write_enable  = port_c_write_req_reg & !port_c_full;
            ram_write_data    = port_c_write_data_reg;
        end
        2'b11: begin
            ram_write_address = {port_select, port_d_wr_ptr[5:0]};
            ram_read_address  = {port_select, port_d_rd_ptr_reg[5:0]};
            ram_write_enable  = port_d_write_req_reg & !port_d_full;
            ram_write_data    = port_d_write_data_reg;
        end
    endcase
end

always_ff @(posedge clk or negedge reset_n) begin
    if (!reset_n) begin
        port_a_read_data <= 32'h0;
        port_b_read_data <= 32'h0;
        port_c_read_data <= 32'h0;
        port_d_read_data <= 32'h0;
    end 
    else begin
        // these reads are 2 clock cycles out of phase becuase the rams have 2cc of latency on a read
        // so the counter will be 2 cc behind what is actually coming out
        case (port_select) 
            2'b00: port_c_read_data <= ram_read_data;
            2'b01: port_d_read_data <= ram_read_data;
            2'b10: port_a_read_data <= ram_read_data;
            2'b11: port_b_read_data <= ram_read_data;
        endcase
    end
end

i3c_dp_ram u_dual_port_ram (
    .clock(clk),
    .data(ram_write_data),
    .rdaddress(ram_read_address),
    .wraddress(ram_write_address),
    .wren(ram_write_enable),
    .q(ram_read_data)
);

endmodule
